package org.dding.core.manager;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

import org.dding.core.IPlatform;
import org.dding.core.IPlatform.PlatformInfo;
import org.dding.core.LifecycleBase;
import org.dding.core.exception.LifecycleException;
import org.dding.core.log.ILogger;
import org.dding.core.log.LoggerFactory;
import org.dding.core.management.DependencyInfo;
import org.dding.core.management.ModuleInfo;
import org.dding.core.management.ResourceLoaderInfo;
import org.dding.core.util.StringUtils;
import org.jboss.modules.ClassPathModuleLoader;
import org.jboss.modules.DefaultBootModuleLoaderHolder;
import org.jboss.modules.Dependency;
import org.jboss.modules.JarModuleLoader;
import org.jboss.modules.LocalDependency;
import org.jboss.modules.LocalLoader;
import org.jboss.modules.Module;
import org.jboss.modules.ModuleClassLoader;
import org.jboss.modules.ModuleDependency;
import org.jboss.modules.ModuleIdentifier;
import org.jboss.modules.ModuleLoadError;
import org.jboss.modules.ModuleLoadException;
import org.jboss.modules.ModuleLoader;
import org.jboss.modules.ModuleNotFoundException;
import org.jboss.modules.ModuleXmlParser;
import org.jboss.modules.ResourceLoader;
import org.jboss.modules.log.JDKModuleLogger;

public class ModuleManager extends LifecycleBase implements IModuleManager {

	protected String modulePath = null;
	protected String configPath = null;
	protected String classpath = null;
	protected boolean jar = false;
	protected String moduleIdentifierOrExeName = null;
	protected boolean classpathDefined = false;
	protected boolean classDefined = false;
	protected ModuleIdentifier logManagerModuleIdentifier = null;
	protected ModuleIdentifier mbeanServerBuilderModuleIdentifier = null;
	protected ModuleIdentifier jaxpModuleIdentifier = null;
	protected String deps = null;
	private static final String[] NO_STRINGS = new String[0];
	private ModuleLoader moduleLoader = null;
	private static final RuntimePermission MODULE_UNLOAD_ANY_PERM = new RuntimePermission(
			"canUnloadAnyModule");
	private ILogger logger = LoggerFactory.getLogger(getClass());

	private IServiceManager services[] = new IServiceManager[0];

	private ConcurrentHashMap<String, List<String>> dependI = new ConcurrentHashMap<String, List<String>>();
	private ConcurrentHashMap<String, List<String>> iDepend = new ConcurrentHashMap<String, List<String>>();

	public ModuleManager(String moduleRoot) {
		setModulePath(moduleRoot);
	}

	@Override
	public Module loadModule(String moduleIdentifer, boolean unzip)
			throws IOException, ModuleLoadException {
		moduleIdentifierOrExeName = moduleIdentifer;
		if (moduleIdentifierOrExeName == null) {
			if (classDefined || classpathDefined) {
				System.err.println("No class name specified");
			} else if (jar) {
				System.err.println("No JAR specified");
			} else {
				System.err.println("No module specified");
			}
			// System.exit(1);
			return null;
		}

		final ModuleLoader loader;
		String[] moduleArgs = NO_STRINGS;
		final ModuleLoader environmentLoader;
		if (configPath != null) {
			environmentLoader = ModuleXmlParser.parseModuleConfigXml(new File(
					configPath));
		} else {
			environmentLoader = DefaultBootModuleLoaderHolder.INSTANCE;
		}
		final ModuleIdentifier moduleIdentifier;
		if (jar) {
			loader = new JarModuleLoader(environmentLoader, new JarFile(
					moduleIdentifierOrExeName));
			moduleIdentifier = ((JarModuleLoader) loader).getMyIdentifier();
		} else if (classpathDefined || classDefined) {
			loader = new ClassPathModuleLoader(environmentLoader,
					moduleIdentifierOrExeName, classpath, deps);
			moduleIdentifier = ModuleIdentifier.CLASSPATH;
		} else {
			loader = environmentLoader;
			moduleIdentifier = ModuleIdentifier
					.fromString(moduleIdentifierOrExeName);
		}
		Module.initBootModuleLoader(loader);
		if (logManagerModuleIdentifier != null) {
			final ModuleClassLoader classLoader = loader.loadModule(
					logManagerModuleIdentifier).getClassLoaderPrivate();
			final ClassLoader old = setContextClassLoader(classLoader);
			try {
				Module.setModuleLogger(new JDKModuleLogger());
			} finally {
				setContextClassLoader(old);
			}
		}
		if (mbeanServerBuilderModuleIdentifier != null) {
			final ModuleClassLoader classLoader = loader.loadModule(
					mbeanServerBuilderModuleIdentifier).getClassLoaderPrivate();
			String name = getServiceName(classLoader,
					"javax.management.MBeanServerBuilder");

			if (name != null) {
				System.setProperty("javax.management.builder.initial", name);
				final ClassLoader old = setContextClassLoader(classLoader);
				try {
					// Initialize the platform mbean server
					ManagementFactory.getPlatformMBeanServer();
				} finally {
					setContextClassLoader(old);
				}
			} else {
				System.err
						.println("WARNING: No mbeanserver service descriptor found in specified mbeanserverbuildermodule "
								+ mbeanServerBuilderModuleIdentifier);
			}
		}
		moduleLoader = loader;
		return loadModule(moduleIdentifier, moduleArgs, unzip);
	}

	private Module loadModule(final ModuleIdentifier moduleIdentifier,
			String[] moduleArgs, boolean unzip) throws ModuleLoadException {
		final Module module;
		try {
			module = getModuleLoader().loadModule(moduleIdentifier);
			List<DependencyInfo> infos = getDependencies(module);
			for (DependencyInfo info : infos) {
				if (!StringUtils.isEmpty(info.getModuleName())) {
					List<String> dep = dependI.get(info.getModuleName());
					if (dep == null) {
						dep = new ArrayList<String>();
					}
					dep.add(module.getIdentifier().toString());
					dependI.put(info.getModuleName(), dep);

					List<String> idepends = iDepend.get(info.getModuleName());
					if (idepends == null) {
						idepends = new ArrayList<String>();
					}
					idepends.add(info.getModuleName());
					iDepend.put(module.getIdentifier().toString(), idepends);
				}
			}
			for (IServiceManager manager : services) {
				try {
					manager.loadServices(moduleIdentifier.toString(), unzip);
				} catch (IOException e) {
					logger.warn("load services failure,when load module"
							+ moduleIdentifier.toString());
					continue;
				}
			}
		} catch (ModuleNotFoundException e) {
			e.printStackTrace(System.err);
			return null;
		}
		try {
			if (!StringUtils.isEmpty(module.getMainClass()))
				module.run(moduleArgs);
		} catch (Throwable e) {
			e.printStackTrace(System.err);
		}
		return module;
	}

	@Override
	public boolean loadModule(String moduleIdentifer) throws Throwable {
		Module module = loadModule(moduleIdentifer, false);
		if (module == null)
			return false;
		else
			return true;
	}

	public boolean unloadModule(final String name) {
		return unloadModule(name, true);
	}

	public boolean unloadModule(final String name, boolean delete) {

		List<String> deps = dependI.get(name);
		if (deps != null && deps.size() != 0) {
			StringBuilder message = new StringBuilder();
			message.append("module [");
			for (String module : deps) {
				message.append(ModuleIdentifier.fromString(module));
				message.append(",");
			}
			message.append("] depend on [" + name + "],can't be unloaded.");
			logger.warn(message.toString());
			return false;
		}

		final SecurityManager sm = System.getSecurityManager();
		if (sm != null) {
			sm.checkPermission(MODULE_UNLOAD_ANY_PERM);
		}
		final ModuleLoader loader = getModuleLoader();
		final Module module = loader.findLoadedModuleLocal(ModuleIdentifier
				.fromString(name));
		if (module == null) {
			return false;
		} else {

			for (IServiceManager manager : services) {
				manager.unloadServices(name);
			}

			loader.unloadModuleLocal(module);
			List<String> idepends = iDepend.get(name);
			if (idepends != null && idepends.size() != 0) {
				for (String idepend : idepends) {
					List<String> temp = dependI.get(idepend);
					if (temp != null)
						temp.remove(name);
				}
			}
			System.gc();
			return true;
		}
	}

	private static ClassLoader setContextClassLoader(
			final ClassLoader classLoader) {
		final SecurityManager sm = System.getSecurityManager();
		if (sm != null) {
			return AccessController
					.doPrivileged(new PrivilegedAction<ClassLoader>() {
						public ClassLoader run() {
							return doSetContextClassLoader(classLoader);
						}
					});
		}
		return doSetContextClassLoader(classLoader);
	}

	private static ClassLoader doSetContextClassLoader(
			final ClassLoader classLoader) {
		try {
			return Thread.currentThread().getContextClassLoader();
		} finally {
			Thread.currentThread().setContextClassLoader(classLoader);
		}
	}

	@Override
	public ModuleLoader getModuleLoader() {
		return moduleLoader;
	}

	private static String getServiceName(ClassLoader classLoader,
			String className) throws IOException {
		final InputStream stream = classLoader
				.getResourceAsStream("META-INF/services/" + className);
		if (stream != null) {
			try {
				final BufferedReader reader = new BufferedReader(
						new InputStreamReader(stream));
				String line;
				while ((line = reader.readLine()) != null) {
					final int i = line.indexOf('#');
					if (i != -1) {
						line = line.substring(0, i);
					}
					line = line.trim();
					if (line.length() == 0)
						continue;
					return line;
				}

			} finally {
				try {
					stream.close();
				} catch (IOException ignored) {
					// ignore
				}
			}
		}
		return null;
	}

	public ModuleInfo getModuleDescription(final String name) {
		final List<DependencyInfo> dependencies = getDependencies(name);
		final List<ResourceLoaderInfo> resourceLoaders = getResourceLoaders(name);
		final Module module = findModule(name);
		final LocalLoader fallbackLoader = module.getFallbackLoader();
		final String fallbackLoaderString = fallbackLoader == null ? null
				: fallbackLoader.toString();
		return new ModuleInfo(module.getIdentifier().toString(), dependencies,
				resourceLoaders, module.getMainClass(), module
						.getClassLoaderPrivate().toString(),
				fallbackLoaderString);
	}

	public Module findModule(final String name) {
		try {
			final Module module = getModuleLoader().findLoadedModuleLocal(
					ModuleIdentifier.fromString(name));
			if (module == null) {
				logger.debug("Module " + name + " not found");
			}
			return module;
		} catch (ModuleLoadError e) {
			throw new IllegalArgumentException("Error loading module " + name
					+ ": " + e.toString());
		}
	}

	@Override
	public List<DependencyInfo> getDependencies(final String moduleIdentifer) {
		Module module = findModule(moduleIdentifer);
		return getDependencies(module);
	}

	private List<DependencyInfo> getDependencies(final Module module) {
		Dependency[] dependencies = module.getDependencies();
		if (dependencies == null) {
			return Collections.emptyList();
		}
		ArrayList<DependencyInfo> list = new ArrayList<DependencyInfo>(
				dependencies.length);
		for (Dependency dependency : dependencies) {
			final String dependencyType = dependency.getClass().getSimpleName();
			final String exportFilter = dependency.getExportFilter().toString();
			final String importFilter = dependency.getImportFilter().toString();
			final DependencyInfo info;
			if (dependency instanceof LocalDependency) {
				final LocalDependency localDependency = (LocalDependency) dependency;
				ArrayList<String> pathList = new ArrayList<String>(
						localDependency.getPaths());
				Collections.sort(pathList);
				info = new DependencyInfo(dependencyType, exportFilter,
						importFilter, null, false, localDependency
								.getLocalLoader().toString(), pathList);
			} else if (dependency instanceof ModuleDependency) {
				final ModuleDependency moduleDependency = (ModuleDependency) dependency;
				info = new DependencyInfo(dependencyType, exportFilter,
						importFilter, moduleDependency.getIdentifier()
								.toString(), moduleDependency.isOptional(),
						null, null);
			} else {
				info = new DependencyInfo(dependencyType, exportFilter,
						importFilter, null, false, null, null);
			}
			list.add(info);
		}
		return list;
	}

	public List<ResourceLoaderInfo> getResourceLoaders(
			final String moduleIdentifer) {
		final Module module = findModule(moduleIdentifer);
		final ModuleClassLoader classLoader = module.getClassLoaderPrivate();
		final ResourceLoader[] loaders = classLoader.getResourceLoaders();
		final ArrayList<ResourceLoaderInfo> list = new ArrayList<ResourceLoaderInfo>(
				loaders.length);
		for (ResourceLoader resourceLoader : loaders) {
			list.add(new ResourceLoaderInfo(
					resourceLoader.getClass().getName(), new ArrayList<String>(
							resourceLoader.getPaths())));
		}
		return list;
	}

	private static IPlatform.PlatformInfo platformInfo = new PlatformInfo();

	public PlatformInfo getPlatformInfo() {
		return platformInfo;
	}

	public boolean isClasspathDefined() {
		return classpathDefined;
	}

	public void setClasspathDefined(boolean classpathDefined) {
		this.classpathDefined = classpathDefined;
	}

	public boolean isClassDefined() {
		return classDefined;
	}

	public void setClassDefined(boolean classDefined) {
		this.classDefined = classDefined;
	}

	public String getModuleIdentifierOrExeName() {
		return moduleIdentifierOrExeName;
	}

	public String getModulePath() {
		return modulePath;
	}

	public void setModulePath(String modulePath) {
		System.setProperty("module.path", modulePath);
		this.modulePath = modulePath;
	}

	public String getConfigPath() {
		return configPath;
	}

	public void setConfigPath(String configPath) {
		this.configPath = configPath;
	}

	public String getClasspath() {
		return classpath;
	}

	public void setClasspath(String classpath) {
		this.classpath = classpath;
	}

	public boolean isJar() {
		return jar;
	}

	public void setJar(boolean jar) {
		this.jar = jar;
	}

	@Override
	protected void initInternal() throws LifecycleException {

		final Enumeration<URL> resources;
		try {
			resources = ModuleManager.class.getClassLoader().getResources(
					"META-INF/MANIFEST.MF");
			while (resources.hasMoreElements()) {
				final URL url = resources.nextElement();
				final InputStream stream = url.openStream();
				if (stream != null)
					try {
						final Manifest manifest = new Manifest(stream);
						Attributes mainAttributes = manifest
								.getMainAttributes();
						if (mainAttributes != null
								&& "dding.module".equals(mainAttributes
										.getValue("Specification-Title"))) {
							platformInfo.version = mainAttributes
									.getValue("platform-version");
							platformInfo.name = mainAttributes
									.getValue("platform-name");
							platformInfo.url = mainAttributes
									.getValue("platform-url");
							platformInfo.author = mainAttributes
									.getValue("platform-author");
							platformInfo.date = mainAttributes
									.getValue("platform-date");
						}

					} finally {
						try {
							stream.close();
						} catch (Throwable ignored) {
						}
					}
			}
		} catch (IOException ignored) {
		}
	}

	@Override
	protected void startInternal() throws LifecycleException {
		synchronized (services) {
			for (int i = 0; i < services.length; i++) {
				services[i].start();
			}
		}
	}

	@Override
	protected void stopInternal() throws LifecycleException {
		for (int i = 0; i < services.length; i++) {
			services[i].stop();
		}
	}

	@Override
	public boolean isModuleExist(String module) {
		return moduleLoader.getModuleMap().containsKey(
				ModuleIdentifier.fromString(module));
	}

	public void addServiceManager(IServiceManager service) {

		service.setModuleManager(this);

		synchronized (services) {
			IServiceManager results[] = new IServiceManager[services.length + 1];
			System.arraycopy(services, 0, results, 0, services.length);
			results[services.length] = service;
			services = results;

			if (getState().isAvailable()) {
				try {
					service.start();
				} catch (LifecycleException e) {
					// Ignore
				}
			}
		}
	}

	@Override
	public IServiceManager[] getServiceManagers() {
		return services;
	}
}
